﻿// (c)2010 David Schwarz (david.schwarz@live.com)
// This file is licensed under GNU GPL v2 license, see more at http://www.gnu.org/licenses/gpl-2.0.html
namespace Z2.Editor
{

	#region Using

	using System;
	using System.Collections.Generic;
	using System.Drawing;
	using System.Xml;
	using Z2.Common;

	#endregion

	/// <summary>
	/// Třída reprezentující mapu hry
	/// </summary>
	public class World
	{

		#region Consts

		private const string XML_VERSION = "1.0";
		private const string XML_ENCODING = "UTF-8";

		#endregion

		#region Members

		private string m_author;
		private string m_name;
		private string m_fileName;
		private bool m_isNew;
		private Point m_start;
		private Dictionary<Point, EditorField> m_fields;
		private Dictionary<Point, EditorFieldObject> m_objects;

		#endregion

		#region Constructor

		/// <summary>
		/// Konstruktor pro načtení existující mapy
		/// </summary>
		/// <param name="fileName">Název souboru mapy</param>
		public World(string fileName)
			: this()
		{
			this.m_fileName = fileName;
			this.m_isNew = false;
		}

		/// <summary>
		/// Konstruktor pro vytvoření nové mapy
		/// </summary>
		public World()
		{
			this.m_fields = new Dictionary<Point, EditorField>();
			this.m_objects = new Dictionary<Point, EditorFieldObject>();
			this.m_isNew = true;
		}

		#endregion

		#region Properties

		/// <summary>
		/// Vrací či nastavuje startovní pozici hráče
		/// </summary>
		public Point Start
		{
			get
			{
				return this.m_start;
			}
			set
			{
				this.m_start = value;
			}
		}

		/// <summary>
		/// Vrací <strong>True</strong>, pokud je mapa nová (nově vytvořená & neuložená)
		/// </summary>
		public bool IsNew
		{
			get
			{
				return this.m_isNew;
			}
		}

		/// <summary>
		/// Vrací název souboru, ve kterém jsou uloženy data mapy
		/// </summary>
		public string FileName
		{
			get
			{
				return this.m_fileName;
			}
		}

		public Dictionary<Point, EditorFieldObject> Objects
		{
			get
			{
				return this.m_objects;
			}
		}

		/// <summary>
		/// Vrací seznam všech polí
		/// </summary>
		public Dictionary<Point, EditorField> Fields
		{
			get
			{
				return this.m_fields;
			}
		}

		/// <summary>
		/// Vrací či nastavuje název mapy
		/// </summary>
		public string Name
		{
			get
			{
				return this.m_name;
			}
			set
			{
				this.m_name = value;
			}
		}

		/// <summary>
		/// Vrací či nastavuje jméno autora
		/// </summary>
		public string Author
		{
			get
			{
				return this.m_author;
			}
			set
			{
				this.m_author = value;
			}
		}

		#endregion

		#region Methods

		/// <summary>
		/// Uloží data levelu do souboru <see cref="FileName"/>
		/// </summary>
		public void Save()
		{
			this.Save(this.FileName);
		}

		/// <summary>
		/// Uloží data levelu do souboru <paramref name="fileName"/>
		/// </summary>
		/// <param name="fileName">Nový název souboru</param>
		public void Save(string fileName)
		{
			this.m_fileName = fileName;

			// Hranice mapy
			long boundX = 0;
			long boundY = 0;
			long boundW = 0;
			long boundH = 0;

			// Vytvoření XML dokumentu & hlavičky
			XmlDocument document = new XmlDocument();
			document.PreserveWhitespace = false;
			document.AppendChild(document.CreateXmlDeclaration(World.XML_VERSION, World.XML_ENCODING, string.Empty));
			
			// Vytvoříme si hlavní elementy mapy
			XmlElement map = document.CreateElement(XmlConst.ELM_MAP);
			XmlElement env = document.CreateElement(XmlConst.ELM_FIELDS);
			XmlElement obj = document.CreateElement(XmlConst.ELM_OBJECTS);
			XmlElement elm = null;
			EditorField field = null;

			boundX = long.MaxValue;
			boundY = long.MaxValue;
			boundW = long.MinValue;
			boundH = long.MinValue;

			// Uložení všech tiles
			foreach (Point point in this.m_fields.Keys)
			{
				// Výpočet hranic mapy
				boundX = Math.Min(boundX, point.X);
				boundY = Math.Min(boundY, point.Y);
				boundW = Math.Max(boundW, point.X);
				boundH = Math.Max(boundH, point.Y);

				field = this.m_fields[point];
				elm = null;

				if (field.EditorObject.Type == ObjectType.Enviroment)
				{
					elm = document.CreateElement(XmlConst.ELM_FIELD);
				}
				else throw new InvalidOperationException();

				World.FillBasicData(elm, field, point, document);

				env.AppendChild(elm);
			}

			// Uložení objektů
			foreach (Point point in this.m_objects.Keys)
			{
				field = this.m_objects[point];
				
				if (field.EditorObject.Type == ObjectType.Object)
				{
					EditorFieldObject fieldObject = (EditorFieldObject)field;
					elm = document.CreateElement(XmlConst.ELM_OBJECT);
					elm.Attributes.Append(CreateAttribute(XmlConst.ATT_TYPE, Convert.ToInt32(fieldObject.Type).ToString(), document));
					elm.Attributes.Append(CreateAttribute(XmlConst.ATT_BEND, fieldObject.Bend.ToString(), document));
				}
				else throw new InvalidOperationException();

				World.FillBasicData(elm, field, point, document);

				obj.AppendChild(elm);
			}

			map.AppendChild(env);
			map.AppendChild(obj);
			document.AppendChild(map);
			
			// Povinné rozšíření hranic
			++boundW;
			++boundH;

			// Uložení hodnot mapy
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_AUTHOR, this.Author, document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_NAME, this.Name, document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_STARTX, this.Start.X.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_STARTY, this.Start.Y.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDX, boundX.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDY, boundY.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDW, boundW.ToString(), document));
			map.Attributes.Append(CreateAttribute(XmlConst.ATT_BOUNDH, boundH.ToString(), document));
			
			document.Save(fileName);
			this.m_isNew = false;
		}

		/// <summary>
		/// Metoda vyplní do elementu <paramref name="elm"/> základní data společná všem polím/objektům ve hře
		/// </summary>
		/// <param name="elm"><see cref="XmlNode"/></param>
		/// <param name="field"><see cref="EditorField"/> nebo potomek</param>
		/// <param name="point"><see cref="Point"/>, na kterém se nachází daný objekt</param>
		/// <param name="document"><see cref="XmlDocument"/></param>
		private static void FillBasicData(XmlNode elm, EditorField field, Point point, XmlDocument document)
		{
			elm.Attributes.Append(CreateAttribute(XmlConst.ATT_ID, Convert.ToString(field.ID), document));
			elm.Attributes.Append(CreateAttribute(XmlConst.ATT_X, Convert.ToString(point.X), document));
			elm.Attributes.Append(CreateAttribute(XmlConst.ATT_Y, Convert.ToString(point.Y), document));
			elm.Attributes.Append(CreateAttribute(XmlConst.ATT_DEFINITION, field.EditorObject.Name, document));
		}

		/// <summary>
		/// Pomocná metoda pro vytoření Xml atributu
		/// </summary>
		/// <param name="name">Název atributu</param>
		/// <param name="value">Hodnota atributu</param>
		/// <param name="doc">Instance <see cref="XmlDocument"/></param>
		/// <returns><see cref="XmlAttribute"/></returns>
		private static XmlAttribute CreateAttribute(string name, string value, XmlDocument doc)
		{
			XmlAttribute attribute = doc.CreateAttribute(name);
			attribute.Value = value;
			return attribute;
		}

		/// <summary>
		/// Metoda načte data mapy
		/// </summary>
		public void Load()
		{
			EditorField.Clear();

			XmlDocument document = new XmlDocument();
			document.Load(this.FileName);

			XmlNodeList map = document.GetElementsByTagName(XmlConst.ELM_MAP);
			this.m_author = map[0].Attributes[XmlConst.ATT_AUTHOR].Value;
			this.m_name = map[0].Attributes[XmlConst.ATT_NAME].Value;
			this.m_start = new Point(
				Convert.ToInt32(map[0].Attributes[XmlConst.ATT_STARTX].Value),
				Convert.ToInt32(map[0].Attributes[XmlConst.ATT_STARTY].Value));

			// Získáme všechny elementy tiles a objektů
			XmlNodeList env = document.GetElementsByTagName(XmlConst.ELM_FIELD);
			XmlNodeList obj = document.GetElementsByTagName(XmlConst.ELM_OBJECT);

			// Načteme..
			foreach (XmlNode node in env)
			{
				this.LoadEditorField(node);
			}

			foreach (XmlNode node in obj)
			{
				this.LoadEditorFieldObject(node);
			}
		}

		/// <summary>
		/// Metoda načte objekt z xml <paramref name="node"/>.
		/// </summary>
		/// <param name="node"><see cref="XmlNode"/> obsahující data</param>
		/// <returns><see cref="EditorFieldObject"/></returns>
		private EditorFieldObject LoadEditorFieldObject(XmlNode node)
		{
			EditorFieldObject fieldObject = (EditorFieldObject)this.LoadEditorField(node);
			
			fieldObject.Type = (FieldObjectType) Convert.ToInt32(node.Attributes[XmlConst.ATT_TYPE].Value);
			fieldObject.Bend = Convert.ToInt32(node.Attributes[XmlConst.ATT_BEND].Value);

			return fieldObject;
		}

		/// <summary>
		/// Metoda vytvoří <see cref="EditorField"/> z <see cref="XmlNode"/>
		/// </summary>
		/// <param name="node">Instance <see cref="XmlNode"/></param>
		/// <returns><see cref="EditorField"/></returns>
		private EditorField LoadEditorField(XmlNode node)
		{
			long id = Convert.ToInt64(node.Attributes[XmlConst.ATT_ID].Value);
			int x = Convert.ToInt32(node.Attributes[XmlConst.ATT_X].Value);
			int y = Convert.ToInt32(node.Attributes[XmlConst.ATT_Y].Value);
			string def = node.Attributes[XmlConst.ATT_DEFINITION].Value;

			EditorField field = this.CreateNewByName(node.Name, id, EditorForm.Definitions[def]);
			
			this.Put(x, y, field);

			return field;
		}

		/// <summary>
		/// Vytvoří pole na základě názvu XML uzlu
		/// </summary>
		/// <param name="name">Název XML uzlu</param>
		/// <param name="id">Identifikátor pole/objektu</param>
		/// <param name="obj">Objekt/Pole</param>
		/// <returns><see cref="EditorField"/> nebo potomek</returns>
		private EditorField CreateNewByName(string name, long id, EditorObject obj)
		{
			switch (name)
			{
				case XmlConst.ELM_FIELD:
					return new EditorField(id, obj);
				case XmlConst.ELM_OBJECT:
					return new EditorFieldObject(id, obj);
			}

			throw new NotImplementedException();
		}

		/// <summary>
		/// Metoda smaže pole na pozici <paramref name="x"/>:<paramref name="y"/>
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		public void Delete(int x, int y)
		{
			Point point = new Point(x, y);
			if (this.m_fields.ContainsKey(point))
			{
				this.m_fields[point].Remove();
				this.m_fields.Remove(point);
			}
		}

		/// <summary>
		/// Metoda smaže z mapy objekt
		/// </summary>
		/// <param name="obj">Objekt</param>
		public void DeleteObject(EditorFieldObject obj)
		{
			foreach(Point point in this.m_objects.Keys)
			if (this.m_objects[point] == obj)
			{
				obj.Remove();
				this.m_objects.Remove(point);
				return;
			}
		}

		/// <summary>
		/// Metoda vloží do mapy objekt/tiles
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <param name="field">Pole/Objekt</param>
		public void Put(int x, int y, EditorField field)
		{
			this.Put(x, y, field, field is EditorFieldObject);
		}

		/// <summary>
		/// Metoda vloží na pozici <paramref name="x"/>:<paramref name="y"/> tile
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <param name="field"><see cref="EditorField"/></param>
		public void Put(int x, int y, EditorField field, bool isObject)
		{
			Point point = new Point(x, y);

			if (isObject)
			{
				if (this.m_objects.ContainsKey(point))
				{
					this.m_objects[point] = (EditorFieldObject)field;
					return;
				}

				this.m_objects.Add(point, (EditorFieldObject)field);
			}
			else
			{
				if (this.m_fields.ContainsKey(point))
				{
					this.m_fields[point] = field;
					return;
				}

				this.m_fields.Add(point, field);
			}
		}

		/// <summary>
		/// Metoda vrací pole mapy na dané pozici. Pokud na dané pozici nic není, vrací <strong>null</strong>.
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <returns>Pole mapy</returns>
		public EditorField Get(int x, int y)
		{
			Point point = new Point(x, y);
			if (this.m_fields.ContainsKey(point))
			{
				return this.m_fields[point];
			}

			return null;
		}

		/// <summary>
		/// Metoda vrací objekt na dané pozici
		/// </summary>
		/// <param name="x">X pozice</param>
		/// <param name="y">Y pozice</param>
		/// <returns>Objekt mapy</returns>
		public EditorFieldObject GetObject(int x, int y)
		{
			Point point = new Point(x, y);
			if (this.m_objects.ContainsKey(point))
			{
				return this.m_objects[point];
			}

			return null;
		}

		#endregion

	}
}
